﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace SpiralClock
{
    /// <summary>
    /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
    ///
    /// Step 1a) Using this custom control in a XAML file that exists in the current project.
    /// Add this XmlNamespace attribute to the root element of the markup file where it is 
    /// to be used:
    ///
    ///     xmlns:MyNamespace="clr-namespace:SpiralClock"
    ///
    ///
    /// Step 1b) Using this custom control in a XAML file that exists in a different project.
    /// Add this XmlNamespace attribute to the root element of the markup file where it is 
    /// to be used:
    ///
    ///     xmlns:MyNamespace="clr-namespace:SpiralClock;assembly=SpiralClock"
    ///
    /// You will also need to add a project reference from the project where the XAML file lives
    /// to this project and Rebuild to avoid compilation errors:
    ///
    ///     Right click on the target project in the Solution Explorer and
    ///     "Add Reference"->"Projects"->[Browse to and select this project]
    ///
    ///
    /// Step 2)
    /// Go ahead and use your control in the XAML file.
    ///
    ///     <MyNamespace:SpiralPanel/>
    ///
    /// </summary>
    public class SpiralPanel : Panel
    {
        private const double r1 = 22.52;
        private const double r2 = 18.00;

        private const double a = 4*r2 - 3*r1;
        private const double b = 2*(r2 - r1)/Math.PI;

        private const double x0 = 50;
        private const double y0 = 45.5;

        public static readonly DependencyProperty DayTimeProperty = DependencyProperty.RegisterAttached(
            "DayTime",
            typeof (DateTime),
            typeof (SpiralPanel),
            new FrameworkPropertyMetadata(DateTime.Now, FrameworkPropertyMetadataOptions.AffectsArrange)
            );


        private UIElement _capturedElement;


        private Vector _click;
        private Point _clickPoint;

        private double _correctionAngle;


        private DateTime _currentDateTime;
        private double _currentAngle;
        private Vector _now;
        private Point _nowPoint;
        private double _stabilityFactor;

        static SpiralPanel()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof (SpiralPanel),
                                                     new FrameworkPropertyMetadata(typeof (SpiralPanel)));
        }

        public SpiralPanel()
        {
            Background = Brushes.Transparent;
        }

        public static void SetDayTime(UIElement element, DateTime value)
        {
            element.SetValue(DayTimeProperty, value);
        }

        public static DateTime GetDayTime(UIElement element)
        {
            return (DateTime) element.GetValue(DayTimeProperty);
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            var resultSize = new Size(0, 0);

            foreach (UIElement child in Children)
            {
                child.Measure(availableSize);
                resultSize.Width = Math.Max(resultSize.Width, child.DesiredSize.Width);
                resultSize.Height = Math.Max(resultSize.Height, child.DesiredSize.Height);
            }

            resultSize.Width = double.IsPositiveInfinity(availableSize.Width)
                                   ? resultSize.Width
                                   : availableSize.Width;

            resultSize.Height = double.IsPositiveInfinity(availableSize.Height)
                                    ? resultSize.Height
                                    : availableSize.Height;

            return resultSize;
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            HitTestResult res = VisualTreeHelper.HitTest(this, e.GetPosition(this));
            foreach (UIElement child in Children)
            {
                if (res == null) break;
                var visual = res.VisualHit as Visual;
                if (visual != null && visual.IsDescendantOf(child))
                {
                    _capturedElement = child;
                    Cursor = Cursors.ScrollAll;
                    _currentDateTime = (DateTime) _capturedElement.GetValue(DayTimeProperty);


                    //handling the click offset
                    _nowPoint = CalculatePosition(CalculateAngle(_currentDateTime));

                    _clickPoint = e.GetPosition(this);

                    _clickPoint.Y = ActualHeight - _clickPoint.Y;
                    _clickPoint = GetInversePointTransform(DesiredSize).
                        Transform(_clickPoint);


                    _nowPoint.X = -x0 + _nowPoint.X;
                    _nowPoint.Y = -y0 + _nowPoint.Y;

                    _clickPoint.X = -x0 + _clickPoint.X;
                    _clickPoint.Y = -y0 + _clickPoint.Y;

                    _now.X = _nowPoint.X;
                    _now.Y = _nowPoint.Y;

                    _click.X = _clickPoint.X;
                    _click.Y = _clickPoint.Y;

                    _correctionAngle = Vector.AngleBetween(_now, _click)*Math.PI/180;


                    CaptureMouse();
                    break;
                }
            }
            base.OnMouseLeftButtonDown(e);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (_capturedElement != null)
            {
                _nowPoint = CalculatePosition(CalculateAngle(_currentDateTime));

                _clickPoint = e.GetPosition(this);

                _clickPoint.Y = ActualHeight - _clickPoint.Y;
                _clickPoint = GetInversePointTransform(DesiredSize).
                    Transform(_clickPoint);


                _nowPoint.X = -x0 + _nowPoint.X;
                _nowPoint.Y = -y0 + _nowPoint.Y;

                _clickPoint.X = -x0 + _clickPoint.X;
                _clickPoint.Y = -y0 + _clickPoint.Y;

                _now.X = _nowPoint.X;
                _now.Y = _nowPoint.Y;

                _click.X = _clickPoint.X;
                _click.Y = _clickPoint.Y;

                double angle = Vector.AngleBetween(_now, _click)*Math.PI/180 - _correctionAngle;


                if (Math.Sign(_currentAngle) != Math.Sign(angle) && Math.Abs(angle) > Math.PI/2)
                {
                    if (angle > 0) _stabilityFactor -= 2*Math.PI;
                    else _stabilityFactor += 2*Math.PI;

                    _stabilityFactor %= 4*Math.PI;
                }


                TimeSpan t = AngleToTime(-(angle + _stabilityFactor));

                _capturedElement.SetValue(DayTimeProperty, _currentDateTime.Add(t));


                ArrangeChild(_capturedElement, new Size(ActualWidth, ActualHeight));
                _currentAngle = angle;
            }
            base.OnMouseMove(e);
        }

        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            if (_capturedElement != null)
            {
                _capturedElement = null;
                _currentAngle = 0;
                _stabilityFactor = 0;
                ReleaseMouseCapture();
                Cursor = Cursors.Arrow;
            }
            base.OnMouseLeftButtonUp(e);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach (UIElement child in Children)
            {
                ArrangeChild(child, finalSize);
            }


            return finalSize;
        }


        private void ArrangeChild(UIElement child, Size finalSize)
        {
            var desiredDate = (DateTime) child.GetValue(DayTimeProperty);

            double angle = CalculateAngle(desiredDate);


            Point initPos = CalculatePosition(angle);
            TransformGroup trg = GetPointTransform(finalSize);
            Point newPos = trg.Transform(initPos);
            child.Arrange(new Rect(newPos.X, finalSize.Height - newPos.Y, child.DesiredSize.Width,
                                   child.DesiredSize.Height));
            var renTr = new TransformGroup();
            renTr.Children.Add(new TranslateTransform(-3, -child.DesiredSize.Height/2));
            renTr.Children.Add(new RotateTransform(180 + 180*CalculateRtAngle(desiredDate)/Math.PI));
            var frameworkElement = child as FrameworkElement;
            if (frameworkElement != null) frameworkElement.RenderTransform = renTr;
        }


        private TransformGroup GetPointTransform(Size size)
        {
            double height = size.Height;
            double width = size.Width;

            if (height == 0 || width == 0) return new TransformGroup();

            double widthRatio = width/90.90;
            double heightRatio = height/100.00;
            double minRatio = Math.Min(widthRatio, heightRatio);
            double scale;


            if (minRatio == widthRatio)
                scale = width/90.9;
            else
                scale = height/100;


            double horizontalTranslation = Math.Abs(width - scale*90.9)/2;
            double verticalTranslation = Math.Abs(height - scale*100)/2;

            var scaleTransform = new ScaleTransform(scale, scale);
            var translateTransform = new TranslateTransform(horizontalTranslation, verticalTranslation);

            var trg = new TransformGroup();
            trg.Children.Add(scaleTransform);
            trg.Children.Add(translateTransform);
            return trg;
        }

        private TransformGroup GetInversePointTransform(Size size)
        {
            double height = size.Height;
            double width = size.Width;

            if (height == 0 || width == 0) return new TransformGroup();

            double widthRatio = width/90.90;
            double heightRatio = height/100.00;
            double minRatio = Math.Min(widthRatio, heightRatio);

            double scale;


            if (minRatio == widthRatio)
                scale = width/90.9;
            else
                scale = height/100;


            double horizontalTranslation = Math.Abs(width - scale*90.9)/2;
            double verticalTranslation = Math.Abs(height - scale*100)/2;

            var scaleTransform = new ScaleTransform(1/scale, 1/scale);
            var translateTransform = new TranslateTransform(-horizontalTranslation, -verticalTranslation);

            var trg = new TransformGroup();
            trg.Children.Add(translateTransform);
            trg.Children.Add(scaleTransform);

            return trg;
        }


        private double CalculateAngle(DateTime time)
        {
            TimeSpan timeOfDay = time.TimeOfDay;

            return CalculateAngle(timeOfDay);
        }

        private double CalculateAngle(TimeSpan time)
        {
            TimeSpan timeOfDay = time;

            double secondsFactor = (-timeOfDay.TotalSeconds)*(Math.PI/6)/3600;

            return -3*Math.PI/2 + secondsFactor;
        }

        private double CalculateRtAngle(DateTime time)
        {
            return CalculateRtAngle(time.TimeOfDay);
        }

        private static double CalculateRtAngle(TimeSpan time)
        {
            TimeSpan timeOfDay = time;

            double secondsFactor = (timeOfDay.TotalSeconds)*(Math.PI/6)/3600;

            return Math.PI/2 + secondsFactor;
        }


        private static Point CalculatePosition(double θ)
        {
            double r = a + b*θ;

            double x = r*Math.Cos(θ) + x0;
            double y = r*Math.Sin(θ) + y0;


            return new Point(x, y);
        }


        private static TimeSpan AngleToTime(double angle)
        {
            return new TimeSpan(0, 0, (int) (6*3600*angle/Math.PI));
        }
    }
}